package com.hxzy.common.aspect;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.hxzy.common.annon.Excel;
import lombok.extern.log4j.Log4j2;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * excel导出工具类
 */
@Log4j2
public class ExcelUtil<T> {

    /**
     * Excel sheet最大行数，默认65536,超过导出行数，新建一个Sheet
     */
    public static final int sheetSize = 65536;

    /**
     * 工作表名称(可不赋值)
     */
    private String sheetName;

    /**
     * 工作薄对象
     */
    private Workbook wb;

    /**
     * 工作表对象
     */
    private Sheet sheet;


    /**
     * 样式列表
     */
    private Map<String, CellStyle> styles;

    /**
     * 导入导出数据列表
     */
    private List<T> list;

    /**
     * 注解列表
     */
    private List<Object[]> fields;

    /**
     * 当前行号
     */
    private int rownum;

    /**
     * 标题
     */
    private String title;

    /**
     * 数字格式(数字保留2位小数)
     */
    private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");

    /**
     * 实体对象
     */
    public Class<T> clazz;

    public ExcelUtil(Class<T> clazz) {
        this.clazz = clazz;
    }

    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @param response 返回数据
     * @param list 导出数据集合
     * @param sheetName 工作表的名称
     * @param title 标题
     * @return 结果
     */
    public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String title)
    {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");

        // 收集类中的自定注解,列名，数据格式等
        this.init(list, sheetName, title);
        exportExcel(response);
    }

    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @return 结果
     */
    private void exportExcel(HttpServletResponse response)
    {
        try
        {
            // 把数据写入到excel表中
            writeSheet();
            //输出到浏览器里面
            wb.write(response.getOutputStream());
        }
        catch (Exception e)
        {
            log.error("导出Excel异常{}", e.getMessage());
        }
        finally
        {
            IOUtils.closeQuietly(wb);
        }
    }




    /**
     * 收集参数 收集类中的自定注解,列名，数据格式等
     * @param list
     * @param sheetName
     * @param title
     */
    private void init(List<T> list, String sheetName, String title){
        if (list == null) {
            list = new ArrayList<T>();
        }
        this.list = list;
        this.sheetName = sheetName;
        this.title = title;

        //反射获取实体类的Excel字段
        this.createExcelField();

        //创建一个工作簿
        this.createWorkbook();
        //创建excel第一行标题
        this.createTitle();
    }

    /**
     * 第1步，反射，获取实体类中的有定义字段（包含@Excel注解的）
     */
    private void createExcelField()
    {
        //获取到所有@Excel注解的字段和@Excel注解 List<Object[0字段, 1 excel注解]>  二维数据
        this.fields = getFields();
        //排字 excel中的sort来排序
        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());

    }
    /**
     *第2步， 创建一个工作簿
     */
    private void createWorkbook()
    {
        this.wb = new SXSSFWorkbook(500);
        // 创建一个工作表Sheet
        this.sheet = wb.createSheet();
        // 为工作表设定名称
        wb.setSheetName(0, sheetName);
        this.styles = createStyles(wb);
    }

    /**
     * 第3步，创建excel第一行标题
     */
    private void createTitle()
    {
        // 如果 title值不为空，第1行就必须要空出来做标题（合并单元格）
        if (StrUtil.isNotEmpty(title))
        {
            // 为何要这里面做一个判断呢？
            Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
            //设定行的高度 30像素
            titleRow.setHeightInPoints(30);
            Cell titleCell = titleRow.createCell(0);
            //得到标题的样式
            titleCell.setCellStyle(styles.get("title"));
            titleCell.setCellValue(title);
            // 合并单元格
            sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(),
                    this.fields.size() - 1));
        }
    }




    /**
     * 获取字段注解信息
     */
    private List<Object[]> getFields()
    {
        List<Object[]> fields = new ArrayList<Object[]>();

        List<Field> tempFields = new ArrayList<>();

        // 得到当前Class的父类的所有的变量  public, protected, default (package) access, and private fields(有可能有)
        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
        // 获取当前Class的所有的变量  public, protected, default (package) access, and private fields
        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));

        //遍历所有的字段，判断字段上面是否有@Excel注解，如果有，就是我们关心的问题
        for (Field field : tempFields)
        {
            // 单注解
            if (field.isAnnotationPresent(Excel.class))
            {
                Excel attr = field.getAnnotation(Excel.class);
                //有注解
                if (attr != null )
                {
                    //私有字段不允许访问，强制开启可以访问
                    field.setAccessible(true);
                    fields.add(new Object[] { field, attr });
                }
            }
        }
        return fields;
    }

    /**
     * 创建表格样式
     *
     * @param wb 工作薄对象
     * @return 样式列表
     */
    private Map<String, CellStyle> createStyles(Workbook wb)
    {
        // 写入各条记录,每条记录对应excel表中的一行
        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();

        //创建标题样式
        CellStyle style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        Font titleFont = wb.createFont();
        titleFont.setFontName("Arial");
        titleFont.setFontHeightInPoints((short) 16);
        titleFont.setBold(true);
        style.setFont(titleFont);
        styles.put("title", style);

        // 创建数据样式 (水平，垂直居中，四个框边实现灰色，字体：宋体 10px)
        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setBorderRight(BorderStyle.THIN);
        style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderLeft(BorderStyle.THIN);
        style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderTop(BorderStyle.THIN);
        style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderBottom(BorderStyle.THIN);
        style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        Font dataFont = wb.createFont();
        dataFont.setFontName("Arial");
        dataFont.setFontHeightInPoints((short) 10);
        style.setFont(dataFont);
        styles.put("data", style);


        //header样式
        style = wb.createCellStyle();
        // 复制了数据样式(水平，垂直居中，四个框边实现灰色，字体：宋体 10px)
        style.cloneStyleFrom(styles.get("data"));

        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        Font headerFont = wb.createFont();
        headerFont.setFontName("Arial");
        headerFont.setFontHeightInPoints((short) 10);
        headerFont.setBold(true);
        headerFont.setColor(IndexedColors.WHITE.getIndex());
        style.setFont(headerFont);
        styles.put("header", style);

        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        Font totalFont = wb.createFont();
        totalFont.setFontName("Arial");
        totalFont.setFontHeightInPoints((short) 10);
        style.setFont(totalFont);
        styles.put("total", style);

        // 数据左对齐
        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.LEFT);
        styles.put("data1", style);

        // 数据居中对齐
        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.CENTER);
        styles.put("data2", style);

        // 数据右对齐
        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.RIGHT);
        styles.put("data3", style);

        return styles;
    }


    /**
     * 第4步，创建写入数据到Sheet
     */
    private void writeSheet(){
        //单个表存放的数据量超过了65536行，分excel表来存放 取出一共有多少个sheet.
        int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
        //循环要创建多个表以及写入多次标题和数据列
        for (int index = 0; index < sheetNo; index++)
        {
            // 判断多个工作表的情况
            createSheet(sheetNo, index);

            // 产生一行(永久是下标为1)
            Row row = sheet.createRow(rownum);
            int column = 0;
            // 写入各个字段的列头名称
            for (Object[] os : fields)
            {
                Excel excel = (Excel) os[1];
                this.createCell(excel, row, column++);
            }

            //填充集合中的excel数据
            fillExcelData(index, row);
        }
    }

    /**
     * 在某一行中创建列
     * @param excel
     * @param row
     * @param column
     */
    private Cell createCell(Excel excel, Row row, int column) {
       // 创建列
        Cell cell = row.createCell(column);
        // 写入列信息
        cell.setCellValue(excel.title());
        //设定列宽
        sheet.setColumnWidth(column, (int) ((excel.width() + 0.72) * 256));
        // 设定样式
        cell.setCellStyle(styles.get("header"));
        return cell;
    }

    /**
     * 工作sheet数据必须要大于1以上，才会创建多个工作表
     *
     * @param sheetNo sheet数量
     * @param index 序号
     */
    private void createSheet(int sheetNo, int index)
    {
        // 设置工作表的名称.
        if (sheetNo > 1 && index > 0)
        {
            this.sheet = wb.createSheet();
            this.createTitle();
            wb.setSheetName(index, sheetName + index);
        }
    }


    /**
     * 分页填充excel数据
     *
     * @param index 序号
     * @param row 单元格行
     */
    public void fillExcelData(int index, Row row)
    {
        //获取集合中开始下标
        int startNo = index * sheetSize;
        //获取集合中的结束下标
        int endNo = Math.min(startNo + sheetSize, list.size());

        for (int i = startNo; i < endNo; i++)
        {
            //创建行号
            row = sheet.createRow(i + 1 + rownum - startNo);
            // 得到导出对象.
            T vo = (T) list.get(i);
            int column = 0;
            for (Object[] os : fields)
            {
                Field field = (Field) os[0];
                Excel excel = (Excel) os[1];
                this.addCell(excel, row, vo, field, column++);
            }
        }
    }

    /**
     * 添加单元格
     * @param attr excel注解
     * @param row  第几行
     * @param vo   实体类对象
     * @param field  实体字段对象
     * @param column  列号
     * @return
     */
    private Cell addCell(Excel attr, Row row, T vo, Field field, int column)
    {
        Cell cell = null;
        try
        {
                // 创建cell
                cell = row.createCell(column);
                // 设定单元格式居中对齐
                cell.setCellStyle(styles.get("data"));

                // 用于读取对象中的属性
                Object value = field.get(vo);

                //有没有日期格式，使用excel的单元格式样式来处理
                String dateFormat =attr.datePattern();
                // 有没有 (格式如: 0=男,1=女,2=未知)
                String readConverterExp = attr.readConverterExp();

                 //如果是日期格式，并且实体类有值的
                if (StrUtil.isNotEmpty(dateFormat) && Objects.nonNull(value)){
                    cell.setCellValue(parseDateToStr(dateFormat, value));
                }
                else if (StrUtil.isNotEmpty(readConverterExp) && Objects.nonNull(value))
                {
                    // 数据显示转换(格式如: 0=男,1=女,2=未知)
                    cell.setCellValue(convertByExp(value.toString(), readConverterExp));
                }
                else if (value instanceof BigDecimal)
                {
                    cell.setCellValue((((BigDecimal) value).setScale(2, BigDecimal.ROUND_HALF_EVEN)).toString());
                }
                else
                {
                    //实体类字段类型
                    if((field.getType() == Double.class || field.getType() ==Float.class) && value!=null){
                       cell.setCellValue( Double.parseDouble(value.toString()));
                    }else if(field.getType()== Integer.class  && value!=null ){
                        cell.setCellValue( Integer.parseInt(value.toString()));
                    }
                    else{
                        //其它全部转换为字符串
                        cell.setCellValue( value==null? "": value.toString());
                    }
                }
        }
        catch (Exception e)
        {
            log.error("导出Excel失败{}", e);
        }
        return cell;
    }

    /**
     * 格式化不同类型的日期对象
     *
     * @param dateFormat 日期格式
     * @param val 被格式化的日期对象
     * @return 格式化后的日期字符
     */
    public String parseDateToStr(String dateFormat, Object val)
    {
        if (val == null)
        {
            return "";
        }
        String str;
        // java.lang.Date   JDK1.0
        if (val instanceof Date)
        {
            str = DateUtil.format((Date) val, dateFormat);
        }
        // java.time    JDK1.8
        else if (val instanceof LocalDateTime)
        {
            str = DateUtil.format((LocalDateTime)val,dateFormat);
        }
       // java.time    JDK1.8
        else if (val instanceof LocalDate)
        {
            LocalDate ld= (LocalDate) val;
            LocalDateTime localDateTime = ld.atStartOfDay();
            str = DateUtil.format(localDateTime,dateFormat);
        }
        else
        {
            str = val.toString();
        }
        return str;
    }

    /**
     * 解析导出值 0=男,1=女,2=未知
     *
     * @param propertyValue 参数值
     * @param converterExp 翻译注解
     * @return 解析后值
     */
    public static String convertByExp(String propertyValue, String converterExp){
        String[] convertSource = converterExp.split(",");
        // 0=男
        for (String item : convertSource) {
            //itemArray[0] =0   itemArray[1]=男
            String[] itemArray = item.split("=");
             if (itemArray[0].equals(propertyValue)){
                return itemArray[1];
            }
        }
        return "未知";
    }

}
